泛型方法是 LINQ 組成元素中,不可或缺的技術,因為若沒有泛型方法,就很難在 LINQ 中對各種資料來源執行查詢作業。請注意,泛型包括了類別、介面、委派、方法,本篇說明泛型方法,但是「泛型」觀念是共通的,所以看懂這篇,其他幾個也可輕鬆了解。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
目前主流的程式語言,包括 C#、Java、VB,都是強型別語言,也就是編譯時期要求所有物件都必須有明確的型別,這樣有助於程式碼的安全性(減少執行時期不可預期的錯誤,例如轉型失敗、找不到成員等),但是也有其缺點,也就是缺乏彈性!以取最大值為例,下面是強型別標準的寫法:
int Max(int a, int b)
{
if (a > b) return a;
else return b;
}
void Main()
{
int result = Max(4, 8);
result.Dump();
}
//輸出:8
上述程式我們定義了一個 Max 方法,接收兩個 int 型別參數,回傳 int 型別的結果,測試也順利完成,所以這段程式沒有問題!但是有一個缺陷,我沒辦法傳入兩個浮點數做比較,因為浮點數的型別是 float。這個問題早幾年我曾經用來問過公司的應試者,第一種答案很常見,就是改用 Object 型別,但不幸的,這個答案並不妥當,而且實際上簡單改寫成 Object 型別的方法參數並不能正常作業:
object Max(object a, object b)
{
if (a > b) return a;
else return b;
}
void Main()
{
int retInt = (int)Max(4, 8);
retInt.Dump();
float retFloat = (float)Max(4.5F, 8.9F);
retFloat.Dump();
}
上述程式碼看似正常,實際上連編譯都不會過:
原因如同編譯的錯誤訊息所示,大於(>)、小於(<)這種運算子,只支援實值型別(Value Type),不支援參考型別(Reference Type),而 object 正是參考型別!早期較漂亮的答案是,因為比大小是透過物件互相比較,而在 .Net 中,只要有實做 IComparable 介面,就擁有 CompareTo 方法,可以讓兩個物件互相比大小,所以程式可改寫如下:
IComparable Max(IComparable a, IComparable b)
{
if (a.CompareTo( b) > 0) return a;
else return b;
}
void Main()
{
int retInt = (int)Max(4, 8);
retInt.Dump(); //輸出 8
float retFloat = (float)Max(4.5F, 8.9F);
retFloat.Dump(); //輸出8.9
}
這樣寫就漂亮多囉,現在傳入的參數只要有實做 IComparable 介面,都可以回傳相比後較大的物件,但又帶來新的問題:
若傳入兩個都有實做 IComparable 介面的型別,但是卻是不同型別,執行時期會發生 ArgumentException。
每次使用這段程式,都必須進行型別轉換,頻繁的轉型會讓程式的效能低落。
第一個問題可以透過在 Max 方法中,先檢查兩個型別是否相等來解決,但是第二個問題以前是必要之惡,直到 .Net 2.0 提出泛型(generic)機制後,才得以解決,而且還可以連第一個問題一起處理掉。我們看一下用泛型定義後的 Max 方法會變什麼樣子:
T Max<T>(T a, T b) where T : IComparable<T>
{
if (a.CompareTo(b) > 0 ) return a;
else return b;
}
一堆 T ,臉上表情都變成 T_T 了……是的,這就是我以前一開始接觸到泛型的表情。其實這個 T 只是一個「任何」型別的代名詞,我們把他換成 X、Y、GGYY 都沒關係,重點就是,這段程式碼是說,定義一個名為 Max 的方法,呼叫時要告訴我使用的型別(Max<T> 的部分),然後我會把要使用的型別代換到其他 T 的地方。
例如:呼叫 Max 方法,告訴它使用的型別是 int(Max<int>),這個 Max 方法就會等同於:
int Max(int a, int b)
例如:如果是 Max<string>,方法就等於 string Max(string a, string b),以此類推。
那上面的 where 又是怎麼一回事?它叫做「條件約束」,MSDN 的定義:當您定義泛型類別時,可限制用戶端程式碼在執行個體化類別時,型別引數可以使用哪些型別。如果用戶端程式碼嘗試使用條件約束所不允許的型別來執行個體化類別,就會產生編譯時期錯誤。這些限制稱為條件約束。您可以使用 where 內容關鍵字指定條件約束。
上述定義是說泛型類別,但其實泛型方法、委派、介面都支援條件約束,所以在 Max 這個方法中,我們前面有說過,必須限制傳入的參數一定要實做 IComparable 介面,這時候就必須透過條件約束達成目的。嗯,那若我傳入的參數是兩個或更多,要怎麼定義它們的條係約束?請見下面範例程式碼即可了解:
class Base { }
class Test<T, U>
where U : struct
where T : Base, new() { }
條件約束有六種,因為超過本文要講述的範圍,所以請自行參考 MSDN:http://msdn.microsoft.com/zh-tw/library/d5x73970.aspx
接著看一下呼叫端遇到泛型方法時,該怎麼呼叫和接收回傳值:
void Main()
{
int retInt = Max<int>(4, 8);
retInt.Dump(); //輸出 8
float retFloat = Max<float>(4.5F, 8.9F);
retFloat.Dump(); //輸出8.9
object retObject = Max<string>("LEO", "FOO");
retObject.Dump(); //輸出 LEO
}
嗯,其實和剛剛沒用泛型之前好像沒有省到什麼程式碼咧?是的,標準寫法的確沒有,不過在底層卻大不相同!因為泛型可以省下可觀的型別轉換成本。那如果要省程式碼行不行?行,利用前幾天介紹的「型別推斷」功能可以解決喔,順便把定義和呼叫的程式碼合併一起看:
T Max<T>(T a, T b) where T : IComparable<T>
{
if (a.CompareTo(b) > 0 ) return a;
else return b;
}
void Main()
{
var retInt = Max(4, 8);
retInt.Dump(); //輸出 8
var retFloat = Max(4.5F, 8.9F);
retFloat.Dump(); //輸出8.9
var retObject = Max("LEO", "FOO");
retObject.Dump(); //輸出 LEO
}
接收回傳值的變數改用 var,然後呼叫 Max 方法要指定使用型別也省掉了,是不是精簡許多!覺得很奇怪對吧,泛型方法呼叫時不是必須告訴他使用型別嗎,這怎麼可以省略?因為型別推斷功能強大,編譯器在編譯時,若我們未明確指出泛型方法的使用型別,這時它會透過我們呼叫方法所傳入的參數之實際類別,推斷出泛型方法的使用型別,並予以套用。很神奇吧~
至此,泛型方法大概說明完畢了,有心理解 LINQ 的朋友,一定要看懂這篇的內容,因為後續文章馬上就會有相關的應用囉!